﻿#region License
/* Copyright (c) 2011 Maksymilian Mulawa
	
Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:

   1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
   2. Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
   3. The name of the author may not be used to endorse or promote products 
 * derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Remoting.Messaging;
using System.Xml.XPath;
using Weather.com.Client.Exceptions;
using Weather.com.Client.WeatherConditions.Common;
using Weather.com.Client.WeatherConditions.CurrentConditions;
using Weather.com.Client.WeatherConditions.Forecasts;
using Weather.com.Client.WeatherConditions.Location;

/// <summary>
/// Weather.com .NET client abstracts interaction with weather.com free service (http://www.weather.com/services/xmloap.html) 
/// providing weather conditions data. Weather.com .NET client enables retrieving weather conditions for today or weather forecast for up to 4 days in advance.
/// </summary>
namespace Weather.com.Client
{
    /// <summary>
    /// Handles interactions with the weather.com service.
    /// </summary>
    /// <example>
    /// This sample shows how to retrieve todays weather conditions for the city of Warsaw in Poland.
    /// <code>
    /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
    /// List&lt;Location&gt; locations = client.GetLocation("Warsaw,Poland");
    /// Location location = locations[0];
    /// CurrentWeatherInfo conditions = client.GetCurrentConditions(location);
    /// </code>
    /// </example>
    public class WeatherClient : IWeatherClient
    {
        private readonly string _partnerId;
        private readonly string _licenseKey;
        private readonly CustomWebClient _webClient;
        private bool _includePromotionLink = true;
        private WeatherServiceErrorHandler _errorHandler;

        private delegate List<Location> AsyncGetLocationCaller(string locationName);

        private delegate CurrentWeatherInfo AsyncGetCurrentConditionsCaller(LocationBase location);

        private delegate ForecastWeatherInfo AsyncGetWeatherForecastCaller(LocationBase location, int days);

        /// <summary>
        /// Initializes new instance of WeatherClient 
        /// </summary>
        /// <param name="partnerId">Partner Id provided by the weather.com</param>
        /// <param name="licenseKey">License Key provided by the weather.com</param>
        /// <remarks>
        /// Before using the library, you need to register with weather.com to receive Partner Id and License Key 
        /// for interaction with the web service. Visit weather.com and sign-up for the service. 
        /// Required authentication data will arrive to you via email.
        /// </remarks>
        public WeatherClient(string partnerId, string licenseKey)
        {
            Units = UnitsSystem.Standard;
            _partnerId = partnerId;
            _licenseKey = licenseKey;
            _webClient = new CustomWebClient();
            _errorHandler = new WeatherServiceErrorHandler();
        }

        /// <summary>
        /// Sets/gets Timeout value for WeatherClient when querying Weather.com service
        /// </summary>
        /// <example>
        /// Sample shows how to set 1 minute timeout for WeatherClient calls.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// client.Timeout = TimeSpan.FromMinutes(1);
        /// List&lt;Location&gt; locations = client.GetLocation("Madrid,Spain") 
        /// </code>
        /// </example>
        public TimeSpan Timeout
        {
            get { return _webClient.Timeout; }
            set { _webClient.Timeout = value; }
        }
        
        /// <summary>
        /// When set to true Promotion links collection is populate and should be diplayed 
        /// together with the weather conditions information
        /// </summary>
        /// <example>
        /// Sample shows how to retrieve promotional links.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// List&lt;Location&gt; locations = client.GetLocation("Warsaw,Poland");
        /// Location location = locations[0];
        /// CurrentWeatherInfo conditions = client.GetCurrentConditions(location);
        /// PromoLink firstPromoLink = conditions.PromotionalLinks[0];
        /// //firstPromoLink.Position
        /// //firstPromoLink.Title
        /// //firstPromoLink.Url.AbsolutePath
        /// </code>
        /// </example>
        public bool IncludePromotionLink
        {
            get { return _includePromotionLink; }
            set { _includePromotionLink = value; }
        }

        /// <summary>
        /// Allows to get and set Metric or Standard metric systems for the weather conditions data being retrieved.
        /// </summary>
        /// <example>
        /// Sample shows how to set modify units with weater condition data.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// client.Units = UnitsSystem.Standard;
        /// List&lt;Location&gt; locations = client.GetLocation("Warsaw,Poland");
        /// Location location = locations[0];
        /// CurrentWeatherInfo conditions = client.GetCurrentConditions(location);
        /// Console.WriteLine(conditions.Units.TemperatureUnit);
        /// Console.WriteLine(conditions.Units.DistanceUnit);
        /// </code>
        /// </example>
        public UnitsSystem Units { get; set; }

        /// <summary>
        /// Retrieves location data in the XML format 
        /// </summary>
        /// <param name="locationName">location name</param>
        /// <returns>Location search data in the XML format</returns>
        /// <example>
        /// This sample shows how to call the <see cref="GetLocationAsXml"/> method. It retrieves locations XML data for the city of Warsaw in Poland.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// string locationXml = client.GetLocationAsXml("Warsaw,Poland");
        /// </code>
        /// </example>
        public string GetLocationAsXml(string locationName)
        {
            string url = string.Format("http://xoap.weather.com/search/search?where={0}", locationName);
            return GetRawXml(url);
        }

        /// <summary>
        /// Performs location search for provided location name. 
        /// </summary>
        /// <param name="locationName">location name, for the non-US countries should be "Country,City". For US-based cities only city name is required.</param>
        /// <returns>List of locations, or empty collection is nothing has been found</returns>
        /// <example>
        /// This sample shows how to call the <see cref="GetLocation"/> method. It retrieves locations for the city of Warsaw in Poland.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// List&lt;Location&gt; locations = client.GetLocation("Warsaw,Poland");        
        /// </code>
        /// </example>
        public List<Location> GetLocation(string locationName)
        {
            string xml = GetLocationAsXml(locationName);
            XPathNavigator navigator = GetXmlDocumentNavigator(xml);
            XPathNodeIterator iterator = navigator.Select("/search/loc");
            List<Location> locations = new List<Location>();
            while (iterator.MoveNext())
            {
                Location loc = new Location();
                loc.Id = iterator.Current.GetAttribute("id", string.Empty);
                loc.Type = iterator.Current.GetAttribute("type", string.Empty);
                loc.Name = iterator.Current.Value;
                locations.Add(loc);
            }

            return locations;
        }

        /// <summary>
        /// Begins an asynchronous location search operation.
        /// </summary>
        /// <param name="locationName">location name, for the non-US countries should be "Country,City". For US-based cities only city name is required.</param>
        /// <param name="userCallback">An optional asynchronous callback, to be called when the location search is complete.</param>
        /// <param name="stateObject">A user-provided object that distinguishes this particular asynchronous location search request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous location search, which could still be pending.</returns>
        /// <example>
        /// Sample shows how to retrieve location asynchronously.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// IAsyncResult result = client.BeginGetLocation("Warsaw,Poland",null, null);
        /// //Do some work here
        /// // Wait until the operation completes.
        /// result.AsyncWaitHandle.WaitOne();
        /// List&lt;Location&gt; locations = _client.EndGetLocation(result);
        /// </code>
        /// </example>
        public IAsyncResult BeginGetLocation(string locationName, AsyncCallback userCallback, Object stateObject)
        {
            AsyncGetLocationCaller caller = GetLocation;
            return caller.BeginInvoke(locationName, userCallback, stateObject);
        }

        /// <summary>
        /// Waits for the pending asynchronous location search to complete.
        /// </summary>
        /// <param name="result">The reference to the pending asynchronous request to finish.</param>
        /// <returns>List of locations, or empty collection is nothing has been found</returns>
        /// <remarks>
        /// EndLocation  can be called once on every IAsyncResult from BeginGetLocation.
        /// </remarks>
        /// <example>
        /// Sample shows how to retrieve location asynchronously.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// IAsyncResult result = client.BeginGetLocation("Warsaw,Poland",null, null);
        /// //Do some work here
        /// //Wait until the operation completes.
        /// result.AsyncWaitHandle.WaitOne();
        /// List&lt;Location&gt; locations = _client.EndGetLocation(result);
        /// </code>
        /// </example>
        public List<Location> EndGetLocation(IAsyncResult result)
        {
            if (result == null)
            {
                throw new ArgumentNullException("result");
            }

            AsyncGetLocationCaller caller = (AsyncGetLocationCaller)((AsyncResult)result).AsyncDelegate;
            return caller.EndInvoke(result);
        }

        /// <summary>
        /// Retrieves todays weather for given location. 
        /// </summary>
        /// <param name="location">location</param>
        /// <returns>Current conditions data in the XML format</returns>
        public string GetCurrentConditionsAsXml(Location location)
        {
           return GetCurrentConditionsAsXmlByLocation(location);
        }
        
        /// <summary>
        /// Retrives todays weather for given US Zip Code
        /// </summary>
        /// <param name="zipCode">US 5-digit zip code</param>
        /// <returns>Current weather conditions</returns>
        public CurrentWeatherInfo GetCurrentConditions(USZipCode zipCode)
        {
            return GetCurrentConditionsByLocation(zipCode);
        }

        /// <summary>
        /// Begins an asynchronous todays weather conditions retrieval operation for a given US Zip Code . 
        /// </summary>
        /// <param name="zipCode">US 5-digit zip code</param>
        /// <param name="userCallback">An optional asynchronous callback, to be called when the todays weather condition retrieval is complete.</param>
        /// <param name="stateObject">A user-provided object that distinguishes this particular asynchronous todays weather condition retrieval request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous todays weather condition retrieval, which could still be pending.</returns>
        public IAsyncResult BeginGetCurrentConditions(USZipCode zipCode, AsyncCallback userCallback, Object stateObject)
        {
            AsyncGetCurrentConditionsCaller caller = GetCurrentConditionsByLocation;
            return caller.BeginInvoke(zipCode, userCallback, stateObject);
        }

        /// <summary>
        /// Retrives todays weather for given location. 
        /// </summary>
        /// <param name="location">location</param>
        /// <returns>Current weather conditions</returns>
        public CurrentWeatherInfo GetCurrentConditions(Location location)
        {
            return GetCurrentConditionsByLocation(location);
        }

        /// <summary>
        /// Begins an asynchronous todays weather conditions retrieval operation for a given location. 
        /// </summary>
        /// <param name="location">location</param>
        /// <param name="userCallback">An optional asynchronous callback, to be called when the todays weather condition retrieval is complete.</param>
        /// <param name="stateObject">A user-provided object that distinguishes this particular asynchronous todays weather condition retrieval request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous todays weather condition retrieval, which could still be pending.</returns>
        public IAsyncResult BeginGetCurrentConditions(Location location, AsyncCallback userCallback, Object stateObject)
        {
            AsyncGetCurrentConditionsCaller caller = this.GetCurrentConditionsByLocation;
            return caller.BeginInvoke(location, userCallback, stateObject);
        }

        /// <summary>
        /// Waits for the pending asynchronous weather conditions retrieval to complete.
        /// </summary>
        /// <param name="result">The reference to the pending asynchronous request to finish.</param>
        /// <returns>Current weather conditions</returns>
        public CurrentWeatherInfo EndGetCurrentConditions(IAsyncResult result)
        {
            CurrentWeatherInfo currentWeatherInfo = null;
            if (result == null)
            {
                throw new ArgumentNullException("result");
            }

            AsyncGetCurrentConditionsCaller caller = ((AsyncResult)result).AsyncDelegate as AsyncGetCurrentConditionsCaller;
            if (caller != null)
            {
                currentWeatherInfo = caller.EndInvoke(result);
            }

            return currentWeatherInfo;
        }
      
        /// <summary>
        /// Retrieves Weather forecast for today and (days-1) days
        /// </summary>
        /// <param name="location">location</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <returns>Weather forecast in the XML format</returns>
        public string GetWeatherForecastAsXml(Location location, int days)
        {
            return GetWeatherForecastAsXmlByLocation(location, days);
        }

        /// <summary>
        /// Retrieves Weather forecast for today and (days-1) days
        /// </summary>
        /// <param name="zipCode">US 5-digit zip code</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <returns>Weather forecast in the XML format</returns>
        public string GetWeatherForecastAsXml(USZipCode zipCode, int days)
        {
            return GetWeatherForecastAsXmlByLocation(zipCode, days);
        }

        /// <summary>
        /// Retrieves Weather forecast for today and (days-1) days
        /// </summary>
        /// <param name="location">location</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <returns>Weather forceast</returns>
        /// <example>
        /// Sample shows how to get weather forecast for following 2 days.
        /// <code>
        /// WeatherClient client = new WeatherClient("[Partner Id here]", "[License Key here]");
        /// List&lt;Location&gt; locations = client.GetLocation("Warsaw,Poland");
        /// Location location = locations[0];
        /// //when querying service for 3 days, it will return information for today and 2 following days.
        /// int days = 3;
        /// ForecastWeatherInfo weatherForecast = client.GetWeatherForecast(location, days);
        /// </code>
        /// </example>
        public ForecastWeatherInfo GetWeatherForecast(Location location, int days)
        {
            return GetWeatherForecastByLocation(location, days);
        }

        /// <summary>
        /// Retrieves Weather forecast for today and (days-1) days
        /// </summary>
        /// <param name="zipCode">US 5-digit zip code</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <returns>Weather forecast</returns>
        public ForecastWeatherInfo GetWeatherForecast(USZipCode zipCode, int days)
        {
            return GetWeatherForecastByLocation(zipCode, days);
        }

        /// <summary>
        /// Begins an asynchronous weather forecast retrieval operation.
        /// </summary>
        /// <param name="location">location</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <param name="userCallback">An optional asynchronous callback, to be called when the weather forecast retrieval is complete.</param>
        /// <param name="stateObject">A user-provided object that distinguishes this particular asynchronous weather forecast retrieval request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous weather forecast retrieval, which could still be pending.</returns>
        public IAsyncResult BeginGetWeatherForecast(Location location, int days, AsyncCallback userCallback, Object stateObject)
        {
            AsyncGetWeatherForecastCaller caller = GetWeatherForecastByLocation;
            return caller.BeginInvoke(location, days, userCallback, stateObject);
        }

        /// <summary>
        /// Begins an asynchronous weather forecast retrieval operation.
        /// </summary>
        /// <param name="zipCode">US 5-digit zip code</param>
        /// <param name="days">number of days in the forecast, maximum is 5</param>
        /// <param name="userCallback">An optional asynchronous callback, to be called when the weather forecast retrieval is complete.</param>
        /// <param name="stateObject">A user-provided object that distinguishes this particular asynchronous weather forecast retrieval request from other requests.</param>
        /// <returns>An IAsyncResult that represents the asynchronous weather forecast retrieval, which could still be pending.</returns>
        public IAsyncResult BeginGetWeatherForecast(USZipCode zipCode, int days, AsyncCallback userCallback, Object stateObject)
        {
            AsyncGetWeatherForecastCaller caller = GetWeatherForecastByLocation;
            return caller.BeginInvoke(zipCode, days, userCallback, stateObject);
        }

        /// <summary>
        /// Waits for the pending asynchronous weather forecast retrieval to complete.
        /// </summary>
        /// <param name="result">The reference to the pending asynchronous request to finish.</param>
        /// <returns>Weather forecast</returns>
        public ForecastWeatherInfo EndGetWeatherForecast(IAsyncResult result)
        {
            if (result == null)
            {
                throw new ArgumentNullException("result");
            }

            AsyncGetWeatherForecastCaller caller = (AsyncGetWeatherForecastCaller)((AsyncResult)result).AsyncDelegate;
            return caller.EndInvoke(result);
        }

        private CurrentWeatherInfo GetCurrentConditionsByLocation(LocationBase location)
        {
            string xml = GetCurrentConditionsAsXmlByLocation(location);
            XPathNavigator navigator = GetXmlDocumentNavigator(xml);
            _errorHandler.ThrowExceptionIfErrorReturned(navigator);
            CurrentWeatherInfo weatherConditions = new CurrentWeatherInfoFactory().Create(navigator);
            return weatherConditions;
        }

        private string GetCurrentConditionsAsXmlByLocation(LocationBase location)
        {
            string url = string.Format(
                "http://xoap.weather.com/weather/local/{0}?prod=xoap&par={1}&key={2}&cc=*&unit={3}",
                location.Id,
                _partnerId,
                _licenseKey,
                Units.InternalUnitName);
            url = IncludePromotionalLinkInResponse(url);
            return GetRawXml(url);
        }

        private string GetWeatherForecastAsXmlByLocation(LocationBase location, int days)
        {
            string url = string.Format(
                "http://xoap.weather.com/weather/local/{0}?prod=xoap&par={1}&key={2}&dayf={3}&unit={4}",
                location.Id,
                _partnerId,
                _licenseKey,
                days,
                Units.InternalUnitName
                );
            url = IncludePromotionalLinkInResponse(url);
            return GetRawXml(url);
        }

        private ForecastWeatherInfo GetWeatherForecastByLocation(LocationBase location, int days)
        {
            string xml = GetWeatherForecastAsXmlByLocation(location, days);
            XPathNavigator navigator = GetXmlDocumentNavigator(xml);
            _errorHandler.ThrowExceptionIfErrorReturned(navigator);
            ForecastWeatherInfo forecast = new WeatherForecastFactory().Create(navigator, days);
            return forecast;
        }


        private XPathNavigator GetXmlDocumentNavigator(string xml)
        {
            StringReader stringReader = new StringReader(xml); 
            XPathDocument document = new XPathDocument(stringReader);
            return document.CreateNavigator();
        }

        private string IncludePromotionalLinkInResponse(string url)
        {
            string requestUrl = url;
            if(IncludePromotionLink)
            {
                requestUrl += "&link=xoap";
            }
            return requestUrl;
        }

        private string GetRawXml(string url)
        {
            System.Diagnostics.Debug.WriteLine(url);
            return _webClient.DownloadString(url);
        }
    }
}
